//
//  entropy.swift
//  Entropy - an object-oriented database for Swift with native queries
//
//  https://entropy.codeplex.com
//  Copyright (c) 2015 Petr Homola. All rights reserved.
//

class PersistentField : Printable {
    var name:String?
    var parent:PersistentObject?
    func value() -> AnyObject { return "" }
    var description:String {
        return name!
    }
    func SQLRepresentation() -> String {
        return "fld_" + name!
    }
}

class PersistentInt : PersistentField, IntegerLiteralConvertible {
    var x:Int
    init(_ _x:Int) {
        x = _x
    }
    required init(integerLiteral _x:IntegerLiteralType) {
        x = _x
    }
    override var description:String {
        return name == nil ? "\(x)" : super.description
    }
    override func value() -> AnyObject { return x }
    override func SQLRepresentation() -> String {
        return name == nil ? "\(x)" : super.SQLRepresentation()
    }
}

class PersistentFloat : PersistentField, FloatLiteralConvertible {
    var x:Float
    init(_ _x:Float) {
        x = _x
    }
    required init(floatLiteral _x:FloatLiteralType) {
        x = Float(_x)
    }
    override var description:String {
        return name == nil ? "\(x)" : super.description
    }
    override func value() -> AnyObject { return x }
    override func SQLRepresentation() -> String {
        return name == nil ? "\(x)" : super.SQLRepresentation()
    }
}

class PersistentString : PersistentField, StringLiteralConvertible {
    var s:String
    init(_ _s:String) {
        s = _s
    }
    typealias UnicodeScalarLiteralType = String
    typealias ExtendedGraphemeClusterLiteralType = StringLiteralType
    required init(unicodeScalarLiteral _s: UnicodeScalarLiteralType) {
        s = "\(_s)"
    }
    required init(extendedGraphemeClusterLiteral _s: ExtendedGraphemeClusterLiteralType) {
        s = _s
    }
    required init(stringLiteral _s:StringLiteralType) {
        s = _s
    }
    override var description:String {
        return name == nil ? "\"\(s)\"" : super.description
    }
    override func value() -> AnyObject { return s }
    override func SQLRepresentation() -> String {
        if name != nil { return super.SQLRepresentation() }
        let str = s.stringByReplacingOccurrencesOfString("\'", withString: "\'\'")
        return "\'\(str)\'"
    }
}

class PersistentObject {
    var UUID:String?
    required init() {}
    func enumerateFields(block:(String,PersistentField)->Void) {
        let mirrors = reflect(self)
        for i in 0..<mirrors.count {
            let (name, mirror)  = mirrors[i]
            if mirror.value is PersistentField {
                let field = mirror.value as! PersistentField
                block(name, field)
            }
        }
    }
    func addNamesToFields() {
        enumerateFields({ name, field in
            field.name = name
            field.parent = self
        })
    }
}

protocol Predicate {
    func SQLWhereClause() -> String
    func evaluateWithFields(fields:Dictionary<String,AnyObject>) -> Bool
}

class EqPredicate<T:PersistentField> : Predicate, Printable {
    var arg1:T
    var arg2:T
    init(_ a1:T, _ a2:T) {
        arg1 = a1
        arg2 = a2
    }
    var description:String {
        return "\(arg1) == \(arg2)"
    }
    func SQLWhereClause() -> String {
        return "\(arg1.SQLRepresentation()) = \(arg2.SQLRepresentation())"
    }
    func evaluateWithFields(fields:Dictionary<String,AnyObject>) -> Bool {
        let val1:AnyObject! = arg1.name == nil ? arg1.value() : (fields[arg1.name!] ?? "")
        let val2:AnyObject! = arg2.name == nil ? arg2.value() : (fields[arg2.name!] ?? "")
        if let v1 = val1 as? String {
            if let v2 = val2 as? String {
                return v1 == v2
            }
        }
        if let v1 = val1 as? Int {
            if let v2 = val2 as? Int {
                return v1 == v2
            }
        }
        return false
    }
}

class GreaterPredicate<T:PersistentField> : Predicate, Printable {
    var arg1:T
    var arg2:T
    init(_ a1:T, _ a2:T) {
        arg1 = a1
        arg2 = a2
    }
    var description:String {
        return "\(arg1) > \(arg2)"
    }
    func SQLWhereClause() -> String {
        return "\(arg1.SQLRepresentation()) > \(arg2.SQLRepresentation())"
    }
    func evaluateWithFields(fields:Dictionary<String,AnyObject>) -> Bool {
        let val1:AnyObject! = arg1.name == nil ? arg1.value() : (fields[arg1.name!] ?? "")
        let val2:AnyObject! = arg2.name == nil ? arg2.value() : (fields[arg2.name!] ?? "")
        if let v1 = val1 as? String {
            if let v2 = val2 as? String {
                return v1 > v2
            }
        }
        if let v1 = val1 as? Int {
            if let v2 = val2 as? Int {
                return v1 > v2
            }
        }
        return false
    }
}

class LessPredicate<T:PersistentField> : Predicate, Printable {
    var arg1:T
    var arg2:T
    init(_ a1:T, _ a2:T) {
        arg1 = a1
        arg2 = a2
    }
    var description:String {
        return "\(arg1) < \(arg2)"
    }
    func SQLWhereClause() -> String {
        return "\(arg1.SQLRepresentation()) < \(arg2.SQLRepresentation())"
    }
    func evaluateWithFields(fields:Dictionary<String,AnyObject>) -> Bool {
        let val1:AnyObject! = arg1.name == nil ? arg1.value() : (fields[arg1.name!] ?? "")
        let val2:AnyObject! = arg2.name == nil ? arg2.value() : (fields[arg2.name!] ?? "")
        if let v1 = val1 as? String {
            if let v2 = val2 as? String {
                return v1 < v2
            }
        }
        if let v1 = val1 as? Int {
            if let v2 = val2 as? Int {
                return v1 < v2
            }
        }
        return false
    }
}

class LessEqPredicate<T:PersistentField> : Predicate, Printable {
    var arg1:T
    var arg2:T
    init(_ a1:T, _ a2:T) {
        arg1 = a1
        arg2 = a2
    }
    var description:String {
        return "\(arg1) <= \(arg2)"
    }
    func SQLWhereClause() -> String {
        return "\(arg1.SQLRepresentation()) <= \(arg2.SQLRepresentation())"
    }
    func evaluateWithFields(fields:Dictionary<String,AnyObject>) -> Bool {
        let val1:AnyObject! = arg1.name == nil ? arg1.value() : (fields[arg1.name!] ?? "")
        let val2:AnyObject! = arg2.name == nil ? arg2.value() : (fields[arg2.name!] ?? "")
        if let v1 = val1 as? String {
            if let v2 = val2 as? String {
                return v1 <= v2
            }
        }
        if let v1 = val1 as? Int {
            if let v2 = val2 as? Int {
                return v1 <= v2
            }
        }
        return false
    }
}

class AndPredicate : Predicate, Printable {
    var arg1:Predicate
    var arg2:Predicate
    init(_ a1:Predicate, _ a2:Predicate) {
        arg1 = a1
        arg2 = a2
    }
    var description:String {
        return "(\(arg1) && \(arg2))"
    }
    func SQLWhereClause() -> String {
        return "(\(arg1.SQLWhereClause()) AND \(arg2.SQLWhereClause()))"
    }
    func evaluateWithFields(fields:Dictionary<String,AnyObject>) -> Bool {
        return arg1.evaluateWithFields(fields) && arg2.evaluateWithFields(fields)
    }
}

class OrPredicate : Predicate, Printable {
    var arg1:Predicate
    var arg2:Predicate
    init(_ a1:Predicate, _ a2:Predicate) {
        arg1 = a1
        arg2 = a2
    }
    var description:String {
        return "(\(arg1) || \(arg2))"
    }
    func SQLWhereClause() -> String {
        return "(\(arg1.SQLWhereClause()) OR \(arg2.SQLWhereClause()))"
    }
    func evaluateWithFields(fields:Dictionary<String,AnyObject>) -> Bool {
        return arg1.evaluateWithFields(fields) || arg2.evaluateWithFields(fields)
    }
}

func ==(x:PersistentInt, y:PersistentInt) -> Predicate {
    return EqPredicate(x, y)
}

func ==(x:PersistentFloat, y:PersistentFloat) -> Predicate {
    return EqPredicate(x, y)
}

func ==(x:PersistentString, y:PersistentString) -> Predicate {
    return EqPredicate(x, y)
}

func >(x:PersistentInt, y:PersistentInt) -> Predicate {
    return GreaterPredicate(x, y)
}

func >(x:PersistentFloat, y:PersistentFloat) -> Predicate {
    return GreaterPredicate(x, y)
}

func <(x:PersistentInt, y:PersistentInt) -> Predicate {
    return LessPredicate(x, y)
}

func <(x:PersistentFloat, y:PersistentFloat) -> Predicate {
    return LessPredicate(x, y)
}

func <=(x:PersistentInt, y:PersistentInt) -> Predicate {
    return LessEqPredicate(x, y)
}

func <=(x:PersistentFloat, y:PersistentFloat) -> Predicate {
    return LessEqPredicate(x, y)
}

func &&(x:Predicate, y:Predicate) -> Predicate {
    return AndPredicate(x, y)
}

func ||(x:Predicate, y:Predicate) -> Predicate {
    return OrPredicate(x, y)
}

func ==(x:PersistentInt, y:PersistentInt) -> Bool {
    return x.x == y.x
}

func ==(x:PersistentString, y:PersistentString) -> Bool {
    return x.s == y.s
}

func >(x:PersistentFloat, y:PersistentFloat) -> Bool {
    return x.x > y.x
}

func <(x:PersistentFloat, y:PersistentFloat) -> Bool {
    return x.x < y.x
}

func <=(x:PersistentFloat, y:PersistentFloat) -> Bool {
    return x.x <= y.x
}

extension String {
    init(_ s:PersistentString) {
        self = s.s
    }
}

extension Int {
    init(_ x:PersistentInt) {
        self = x.x
    }
}

extension Float {
    init(_ x:PersistentFloat) {
        self = x.x
    }
}

class ObjectPile {
    var database:SQLiteDatabase
    init(fileName:String) {
        database = SQLiteDatabase(fileName: fileName)
        createTables()
    }
    private func createTables() {
        database.executeSQL("CREATE TABLE IF NOT EXISTS objects (uuid TEXT, typename TEXT)");
        database.executeSQL("CREATE TABLE IF NOT EXISTS stringfields (uuid TEXT, fieldname TEXT, fieldvalue TEXT)");
        database.executeSQL("CREATE TABLE IF NOT EXISTS integerfields (uuid TEXT, fieldname TEXT, fieldvalue INTEGER)");
    }
    func store(obj:PersistentObject) {
        var className = toString(obj.dynamicType);
        className = className.stringByReplacingOccurrencesOfString(".", withString: "_")
        var UUID:String!
        if obj.UUID != nil {
            //println("updating existing object")
            let UUIDs = database.querySQL("SELECT uuid FROM objects WHERE uuid = ?", params: [ obj.UUID! ])
            assert(count(UUIDs) == 1, "there must be exactly one object with a given UUID in the database")
            database.executeSQL("DELETE FROM stringfields WHERE uuid = ?", params: [ obj.UUID! ])
            database.executeSQL("DELETE FROM integerfields WHERE uuid = ?", params: [ obj.UUID! ])
            UUID = obj.UUID!
        } else {
            //println("storing new object")
            UUID = NSUUID().UUIDString;
            database.executeSQL("INSERT INTO objects (uuid, typename) VALUES (?, ?)", params: [ UUID, className ])
        }
        obj.enumerateFields({ name, field in
            if (field is PersistentString) {
                //println("string field: \(name)")
                self.database.executeSQL("INSERT INTO stringfields (uuid, fieldname, fieldvalue) VALUES(?, ?, ?)", params: [ UUID, name, field.value() ])
            }
            if (field is PersistentInt) {
                //println("integer field: \(name)")
                self.database.executeSQL("INSERT INTO integerfields (uuid, fieldname, fieldvalue) VALUES(?, ?, ?)", params: [ UUID, name, field.value() ])
            }
        })
    }
    func removeObjectWithUUID(UUID:String) {
        // to be tested in Swift 2
        /*database.executeSQL("BEGIN TRANSACTION")
        var success = false
        defer {
            if (success) {
                database.executeSQL("COMMIT")
                println("commited")
            } else {
                database.executeSQL("ROLLBACK")
                println("rolled back")
            }
        }*/
        database.executeSQL("DELETE FROM objects WHERE uuid = ?", params: [ UUID ])
        database.executeSQL("DELETE FROM stringfields WHERE uuid = ?", params: [ UUID ])
        database.executeSQL("DELETE FROM integerfields WHERE uuid = ?", params: [ UUID ])
    }
    func objectWithUUID<T:PersistentObject>(UUID:String, type Typ:T.Type) -> T? {
        var className = toString(Typ);
        className = className.stringByReplacingOccurrencesOfString(".", withString: "_")
        let rows = database.querySQL("SELECT uuid FROM objects WHERE uuid = ?", params: [ UUID ])
        assert(count(rows) <= 1, "no more than one value allowed per UUID and field name")
        var object:T?
        if count(rows) > 0 {
            object = Typ()
            object!.UUID = UUID;
            object!.enumerateFields({ name, field in
                if let castField = field as? PersistentString {
                    let values = self.database.querySQL("SELECT fieldvalue FROM stringfields WHERE uuid = ? AND fieldname = ?", params: [ UUID, name ])
                    assert(count(values) <= 1, "no more than one value allowed per UUID and field name")
                    if count(values) > 0 {
                        castField.s = values[0][0] as! String
                    }
                }
                if let castField = field as? PersistentInt {
                    let values = self.database.querySQL("SELECT fieldvalue FROM integerfields WHERE uuid = ? AND fieldname = ?", params: [ UUID, name ])
                    assert(count(values) <= 1, "no more than one value allowed per UUID and field name")
                    if count(values) > 0 {
                        castField.x = values[0][0] as! Int
                    }
                }
            })
        }
        return object
    }
    func objectsOfType<T:PersistentObject>(Typ:T.Type) -> Array<T> {
        var className = toString(Typ);
        className = className.stringByReplacingOccurrencesOfString(".", withString: "_")
        let rows = database.querySQL("SELECT uuid FROM objects WHERE typename = ?", params: [ className ])
        var objects = Array<T>()
        for row in rows {
            let UUID = row[0] as! String
            let object = Typ()
            object.UUID = UUID;
            object.enumerateFields({ name, field in
                if let castField = field as? PersistentString {
                    let values = self.database.querySQL("SELECT fieldvalue FROM stringfields WHERE uuid = ? AND fieldname = ?", params: [ UUID, name ])
                    assert(count(values) <= 1, "no more than one value allowed per UUID and field name")
                    if count(values) > 0 {
                        castField.s = values[0][0] as! String
                    }
                }
                if let castField = field as? PersistentInt {
                    let values = self.database.querySQL("SELECT fieldvalue FROM integerfields WHERE uuid = ? AND fieldname = ?", params: [ UUID, name ])
                    assert(count(values) <= 1, "no more than one value allowed per UUID and field name")
                    if count(values) > 0 {
                        castField.x = values[0][0] as! Int
                    }
                }
            })
            objects.append(object)
        }
        return objects
    }
    func query<T:PersistentObject>(block:(T)->Predicate) -> Array<T> {
        var fieldNames = Array<String>()
        fieldNames.append("uuid")
        var className = toString(T);
        className = className.stringByReplacingOccurrencesOfString(".", withString: "_")
        let obj = T()
        let predicate = block(obj)
        obj.addNamesToFields()
        let rows = database.querySQL("SELECT uuid FROM objects WHERE typename = ?", params: [ className ])
        var objects = Array<T>()
        for row in rows {
            let UUID = row[0] as! String
            let object = T()
            var fields = Dictionary<String,AnyObject>()
            object.UUID = UUID;
            object.enumerateFields({ name, field in
                if let castField = field as? PersistentString {
                    let values = self.database.querySQL("SELECT fieldvalue FROM stringfields WHERE uuid = ? AND fieldname = ?", params: [ UUID, name ])
                    assert(count(values) <= 1, "no more than one value allowed per UUID and field name")
                    if count(values) > 0 {
                        castField.s = values[0][0] as! String
                        fields[name] = values[0][0]
                    }
                }
                if let castField = field as? PersistentInt {
                    let values = self.database.querySQL("SELECT fieldvalue FROM integerfields WHERE uuid = ? AND fieldname = ?", params: [ UUID, name ])
                    assert(count(values) <= 1, "no more than one value allowed per UUID and field name")
                    if count(values) > 0 {
                        castField.x = values[0][0] as! Int
                        fields[name] = values[0][0]
                    }
                }
            })
            if (predicate.evaluateWithFields(fields)) { objects.append(object) }
        }
        return objects
    }
}

class ObjectRelationalContainer {
    var database:SQLiteDatabase
    init(fileName:String) {
        database = SQLiteDatabase(fileName: fileName)
    }
    func store(obj:PersistentObject) {
        var className = toString(obj.dynamicType);
        className = className.stringByReplacingOccurrencesOfString(".", withString: "_")
        let records = database.querySQL("SELECT name FROM sqlite_master WHERE name = ?", params: [ className ])
        if records.count == 0 {
            println("table \(className) not found")
            database.executeSQL("CREATE TABLE \(className) (uuid TEXT)")
        }
        var columns = Set<String>()
        for (index, record) in enumerate(database.querySQL("PRAGMA table_info(\(className))")) {
            columns.insert(record[1] as! String)
        }
        var fieldNames = Array<String>(), placeholders = Array<String>(), values = Array<AnyObject>()
        obj.enumerateFields({ name, field in
            let prefixedName = "fld_" + name
            if !columns.contains(prefixedName) {
                println("column \(prefixedName) not found")
                self.database.executeSQL("ALTER TABLE \(className) ADD COLUMN \(prefixedName)")
            }
            fieldNames.append(prefixedName)
            placeholders.append("?")
            values.append(field.value())
        })
        if obj.UUID == nil {
            println("new record")
            obj.UUID = NSUUID().UUIDString
        } else {
            println("existing record")
            database.executeSQL("DELETE FROM \(className) WHERE uuid = ?", params: [ obj.UUID! ])
        }
        fieldNames.append("uuid")
        placeholders.append("?")
        values.append(obj.UUID!)
        let delim = ","
        let sql = "INSERT INTO \(className) (\(delim.join(fieldNames))) VALUES (\(delim.join(placeholders)))"
        database.executeSQL(sql, params: values)
    }
    func objectsOfType<T:PersistentObject>(Typ:T.Type) -> Array<T> {
        var fieldNames = Array<String>()
        fieldNames.append("uuid")
        Typ().enumerateFields({ name, field in
            let prefixedName = "fld_" + name
            fieldNames.append(prefixedName)
        })
        var className = toString(Typ);
        className = className.stringByReplacingOccurrencesOfString(".", withString: "_")
        let delim = ","
        let sql = "SELECT \(delim.join(fieldNames)) FROM \(className)"
        let records = database.querySQL(sql)
        var objects = Array<T>()
        if records == nil { return objects }
        for (index, record) in enumerate(records) {
            var data = Dictionary<String,AnyObject>()
            for var i = 0; i < fieldNames.count; i++ {
                data[fieldNames[i]] = record[i]
            }
            let obj = Typ()
            obj.UUID = data["uuid"] as? String
            obj.enumerateFields({ name, field in
                let prefixedName = "fld_" + name
                let value:AnyObject = data[prefixedName]!
                if let castField = field as? PersistentString {
                    castField.s = value as! String
                } else if let castField = field as? PersistentInt {
                    if let v = value as? Int {
                        castField.x = v
                    } else {
                        castField.x = 0
                    }
                } else if let castField = field as? PersistentFloat {
                    if let v = value as? Float {
                        castField.x = v
                    } else {
                        castField.x = 0
                    }
                }
            })
            objects.append(obj)
        }
        return objects
    }
    func objectWithUUID<T:PersistentObject>(uuid:String, type Typ:T.Type) -> T? {
        var fieldNames = Array<String>()
        fieldNames.append("uuid")
        Typ().enumerateFields({ name, field in
            let prefixedName = "fld_" + name
            fieldNames.append(prefixedName)
        })
        var className = toString(Typ);
        className = className.stringByReplacingOccurrencesOfString(".", withString: "_")
        let delim = ","
        let sql = "SELECT \(delim.join(fieldNames)) FROM \(className) WHERE uuid = ?"
        let records = database.querySQL(sql, params: [ uuid ])
        if records == nil || records.count == 0 { return nil }
        assert(records.count == 1, "more than one record with one UUID: \(uuid)")
        let obj = Typ()
        for (index, record) in enumerate(records) {
            var data = Dictionary<String,AnyObject>()
            for var i = 0; i < fieldNames.count; i++ {
                data[fieldNames[i]] = record[i]
            }
            obj.UUID = uuid //data["uuid"] as? String
            obj.enumerateFields({ name, field in
                let prefixedName = "fld_" + name
                let value:AnyObject = data[prefixedName]!
                if let castField = field as? PersistentString {
                    castField.s = value as! String
                } else if let castField = field as? PersistentInt {
                    if let v = value as? Int {
                        castField.x = v
                    } else {
                        castField.x = 0
                    }
                } else if let castField = field as? PersistentFloat {
                    if let v = value as? Float {
                        castField.x = v
                    } else {
                        castField.x = 0
                    }
                }
            })
        }
        return obj
    }
    func query<T:PersistentObject>(block:(T)->Predicate) -> Array<T> {
        var fieldNames = Array<String>()
        fieldNames.append("uuid")
        let obj = T()
        obj.enumerateFields({ name, field in
            let prefixedName = "fld_" + name
            fieldNames.append(prefixedName)
        })
        var className = toString(T);
        className = className.stringByReplacingOccurrencesOfString(".", withString: "_")
        let delim = ","
        let predicate = block(obj)
        obj.addNamesToFields()
        let sql = "SELECT \(delim.join(fieldNames)) FROM \(className) WHERE " + predicate.SQLWhereClause()
        println("SQL query: \(sql)")
        let records = database.querySQL(sql)
        var objects = Array<T>()
        if records == nil { return objects }
        for (index, record) in enumerate(records) {
            var data = Dictionary<String,AnyObject>()
            for var i = 0; i < fieldNames.count; i++ {
                data[fieldNames[i]] = record[i]
            }
            let obj = T()
            obj.UUID = data["uuid"] as? String
            obj.enumerateFields({ name, field in
                let prefixedName = "fld_" + name
                let value:AnyObject = data[prefixedName]!
                if let castField = field as? PersistentString {
                    castField.s = value as! String
                } else if let castField = field as? PersistentInt {
                    if let v = value as? Int {
                        castField.x = v
                    } else {
                        castField.x = 0
                    }
                } else if let castField = field as? PersistentFloat {
                    if let v = value as? Float {
                        castField.x = v
                    } else {
                        castField.x = 0
                    }
                }
            })
            objects.append(obj)
        }
        return objects
    }
}
